// Note that if you run the SWF outside of a browser, it doesn't use
// the cache very well. That is, if you have the same image being
// used twice, it will download it twice. Running it in the browser
// works fine so we don't address this issue.
//
package com.frontalcode 
{
	import flash.events.*;
	
	/**
	 * The AssetManager class is part of Frontal's asset loading and management 
	 * framework. Typically it is not used directly but is rather support for
	 * the img and include tags among other things.
	 */
    public class AssetManager extends OptionableObject
    {
		static private var _instance : AssetManager = null;
		
		/*
			Variables: queue variables
			
				These arrays are used to manage the pending, loading and done 
				objects as well as maintain a list of all the objects. pending,
				loading and done are arrays of asset ids. all is an associative
				array keyed by asset ids.
		*/
		
		/**
		 * @private 
		 */
		internal var demanded : Array = [ ];
		
		/**
		 * @private 
		 */
		internal var pending : Array = [ ];
		
		/**
		 * @private 
		 */
		internal var loading : Array = [ ];
		
		/**
		 * @private 
		 */
		internal var done : Array = [ ];
		
		/**
		 * @private 
		 */
		internal var all : Object = { };
		
		
		/**
		 * @private 
		 */
		internal var bytesLoaded : Number = 0;
		
		/**
		 * @private 
		 */
		internal var loadingSecs : Number = 0;
		
		/**
		 * @private 
		 */
		internal var taskId : * = null;
		
		/**
		 * @private 
		 */
		internal var _pause : Boolean = false;
		
		/**
		 * @private 
		 */
		internal var demandLoadingEnabled : Boolean = true;
		
		/**
		 * @private 
		 */
		internal var baseURL : String;
		
		/**
		 * Creates a new AssetManager instance.  
		 *
		 * <p>Options may be set through the constructor, the movie's URL or the
		 * movie's FlashVars parameter. The following options are supported:
		 * 	<table class="innertable">
		 * 		<tr><th>Option</th><th>Default</th><th>Description</th></tr>
		 * 		<tr><td>assetMgrLoadConnections</td><td>20</td><td>The number of asset loads that will be attempted at once.</td></tr>
		 * 		<tr><td>assetMgrLoadLimitInBytes</td><td>Number.MAX_VALUE</td><td>The number of bytes that will be loaded before assets will begin to be automatically unloaded.</td></tr>
		 * 		<tr><td>assetMgrMaxFailuresPerAsset</td><td>10</td><td>The number of times an asset will be attempted to be loaded before finally failing.</td></tr>
		 * 	</table>
		 * </p>  
		 *
		 * @param options		Options affecting the behavior of this instance. 
		 *
		 * @throws com.frontalcode.FrontalError Only one instance of this class may be created.
		 */  
		public function AssetManager ( options : Object = null )
		{
			super ( options );
			
			if ( _instance != null ) throw new FrontalError ( "Document manager already initialized" );
			
			setObjectOptionDefault ( "assetMgrLoadConnections", 2 );
			setObjectOptionDefault ( "assetMgrLoadLimitInBytes", Number.MAX_VALUE );
			setObjectOptionDefault ( "assetMgrMaxFailuresPerAsset", 10 );
			setObjectOptionDefault ( "assetMgrInitiallyPaused", false );
			setOptionsFromLoaderParameters ( );
	
			_instance = this;
		}
		
		/**
		 * Returns the single instance of the DocumentManager class.
		 *
		 * @return The document manager.
		 */
		static public function getInstance ( ) : AssetManager
		{
			if ( _instance == null ) throw new FrontalError ( "Asset manager not initialized" );
			return _instance;
		}
		
		/**
		 * Returns the single instance of the DocumentManager class. Equivalent to getInstance().
		 *
		 * @return The document manager.
		 */
		static public function gI ( ) : AssetManager { return getInstance ( ); }
		
		/*
			Function: getFullURL
			
				This function prefixes the raw URL with a base URL but only if
				the URL does not .
			
			Parameters:
			
				rawURL	 	- the raw URL to decorate
				baseURL 	- base URL to apply
		*/
		/**            
		 * @private
		 */
		public function getFullURL ( rawURL : String, baseURL : String = null ) : String
		{
			if ( baseURL != null && ! Util.isAbsoluteURL.test ( rawURL ) )
			{
				if ( baseURL.charAt ( baseURL.length - 1 ) != '/' ) baseURL += '/';
				if ( rawURL.charAt ( 0 ) == '/' ) rawURL = rawURL.substr ( 1 );
				return baseURL + rawURL;
			}
			
			return rawURL;
		}
	
		/**            
		 * @private
		 */
		internal function checkLoadLimit ( ) : void
		{
			if ( bytesLoaded > getObjectOption ( "assetMgrLoadLimitInBytes" ) )
			{
				// Unload less populate movies until we are under our limit.
				//
				
				while ( done.length > 0 && bytesLoaded > getObjectOption ( "assetMgrLoadLimitInBytes" ) )
				{
					var noCandidates : Boolean = true;
					
					for ( var i : int = 0; i < done.length; i++ )
					{
						var loadObject : Asset = all [ done [ i ] ];
						
						// Only target invisible movies. (These will not have a
						// stage.)
						//
						
						if ( loadObject.loader.stage == null )
						{
							noCandidates = false;
							bytesLoaded -= loadObject.bytesLoaded;
							loadObject.unload ( );
							pending.push ( done [ i ] );
							enableStatusTask ( true );
							done.splice ( i, 1 );
						}
					}
					
					if ( noCandidates )
					{
						break;
					}
				}
			}
		}
	
		/*
			 Function: enableDemandLoading
				
				While demand loading is disabled, any call to "demandObject" will be
				ignored. The reason for this function is that stage managed sites tend
				to be created from the first section towards the last. If these sections
				are then shown in this order (as is the case with certain transitions
				like the sliding poster transition) and each section demands its images
				as they are begin shown, then the loading queue will end up favoring
				later sections over earlier ones. So the technique is to disabled
				demand loading during setup and then enable it when the site becomes
				interactive.
				
			Parameters:
			
				enable - boolean
			
			Returns:
			
				previous state
		*/
		/**            
		 * @private
		 */
		internal function enableDemandLoading ( enable:Boolean ) : Boolean
		{
			var previousState:Boolean = demandLoadingEnabled;
			
			demandLoadingEnabled = enable;
			
			return previousState;
		}
		
		/*
			Function: demandObject
		
			Parameters:
			
				loadObject - loadObject
			
			Returns:
			
				loadObject
		*/
		public function demandObject ( loadObject : Asset ) : Asset
		{
			if ( demandLoadingEnabled ) demanded.push ( loadObject );
			
			return loadObject;
		}
		
		internal function processDemandObjects ( ) : void
		{		
			// If we've hit our load limit then try to clear some room.
			//
			
			checkLoadLimit ( );
	
			// We're willing to remove anything not demanded.
			//
			
			var removalCandidates : Array = [ ];
			for ( var i : int = 0; i < loading.length; i++ )
			{
				if ( demanded.indexOf ( all [ loading [ i ] ] ) < 0 ) removalCandidates.push ( all [ loading [ i ] ] );
			}
						
			for ( i = 0; i < demanded.length; i++ )
			{
				var loadObject : Asset = demanded [ i ];
			
				// If it's already loaded, then move it to the top of the done 
				// queue to indicate its popularity.
				//
				
				if ( loadObject.state == Asset.LOADED ) 
				{
					for ( j = 0; j < done.length; j++ )
					{
						if ( done [ j ] == loadObject.id )
						{
							done.splice ( j, 1 );
							done.push ( loadObject.id );
							break;
						}
					}
					
					continue;
				}
				
				var alreadyLoading : Boolean = false;
				for ( var j : uint = 0; j < loading.length; j++ )
				{
					if ( loading [ j ] == loadObject.id )
					{
						alreadyLoading = true;
						break;
					}
				}
				if ( alreadyLoading ) continue;
				
				if ( loading.length >= getObjectOption ( "assetMgrLoadConnections" ) ) 
				{
					// If the loading queue is full, remove something from 
					// it. We prefer things that haven't actually started 
					// loading yet. Also, we only remove things that aren't
					// in the demanded queue.
					//
					
					var removedOne : Boolean = false;
					
					for ( j = 0; j < removalCandidates.length; j++ )
					{
						if ( removalCandidates [ j ].state != Asset.LOADING )
						{
							cancelObjectLoad ( removalCandidates [ j ] );
							removalCandidates.splice ( j, 1 );
							removedOne = true;
							break;
						}
					}
				
					if ( ! removedOne && removalCandidates.length > 0 )
					{
						cancelObjectLoad ( removalCandidates [ 0 ] );
						removalCandidates.splice ( 0, 1 );
					}
				}
				
				// Now let's move it to the top of the pending queue where the
				// loader task will pick it up next.
				//
			
				for ( j = 0; j < pending.length; j++ )
				{
					if ( pending [ j ] == loadObject.id )
					{
						pending.splice ( j, 1 );
						pending.push ( loadObject.id );
						enableStatusTask ( true );
						
						break;
					}
				}
			}
			
			demanded = [ ];
		}
	
		/*
			Function:  preloadObject
				
				adds an object to the pending queue
			
			Parameters:
			
				loadObject - object to preload
		*/
		public function preloadObject ( loadObject : Asset ) : void
		{
			if ( all [ loadObject.id ] != null )
			{
				// Already processed but move it to the top of the done queue to
				// indicate its popularity.
				//
				// Or if it's in the done queue because we've given up on it then
				// move it to our pending queue.
				//
				
				if ( loadObject.state == Asset.LOADED )
				{
					for ( var i : uint = 0; i < done.length; i++ )
					{
						if ( done [ i ] == loadObject.id )
						{
							done.splice ( i, 1 );
							
							if ( loadObject.state == Asset.FAILED )
							{
								loadObject.reset ( );
								pending.unshift ( loadObject.id );
								enableStatusTask ( true );
							}
							else
							{
								done.push ( loadObject.id );
							}
							
							break;
						}
					}
				}
			}
			else
			{
				all [ loadObject.id ] = loadObject;
				
				pending.unshift ( loadObject.id );
				enableStatusTask ( true );
			}
		}
	
		/*
			Function: enableStatusTask
			
				Start or stop the task to check the load status.
		
			Parameters:
			
				enable - true to start the task otherwise false
		*/
		/**            
		 * @private
		 */
		internal function enableStatusTask ( enable : Boolean ) : void
		{
			if ( enable )
			{
				// This is a NOP if the taskId is of an already scheduled task.
				//
				
				taskId = Scheduler.getInstance ( ).scheduleTask ( taskId, checkLoadStatus );
			}
			else
			{
				taskId = Scheduler.getInstance ( ).removeTask ( taskId );
			}
		}
		
		/*
			Function: cancelObjectLoad
		
			Parameters:
			
				loadObject - loadObject
		*/
		/**            
		 * @private
		 */
		internal function cancelObjectLoad ( loadObject : Asset ) : void
		{
			if ( loadObject.state == Asset.LOAD_STARTED || loadObject.state == Asset.LOADING )
			{
				loadObject.cancelLoad ( );
				
				for ( var i : uint = 0; i < loading.length; i++ )
				{
					if ( loading [ i ] == loadObject.id )
					{
						// Put it in place to load next as it had drifted to the
						// top of the load queue when it was canceled due to
						// something else being demanded.
						//
						loading.splice ( i, 1 );
						pending.push ( loadObject.id );
						enableStatusTask ( true );
						break;
					}
				}
			}
		}
	
		/*
			Function: loadPendingObject
			
				Start loading aas many as objectsToLoad pending assets.
				
			Parameters:
			
				objectsToLoad	- Maximum number of assets to start loading
		*/	
		/**            
		 * @private
		 */
		internal function loadPendingObject ( objectsToLoad : uint ) : void
		{
			while ( pending.length > 0 && objectsToLoad-- > 0 )
			{
				var loadObject : Asset = all [ pending.pop ( ) ];
				loading.push ( loadObject.id );
				loadObject.startLoad ( );
			}
		}
	
		/*
			Function: unloadObject
			
				Unload the object and remove it from our queues.
		
			Parameters:
			
				loadObject - loadObject
		*/
		public function unloadObject ( loadObject : Asset ) : void
		{
			delete all [ loadObject.id ];
			loadObject.unload ( );
			
			var i : uint = pending.indexOf ( loadObject.id );
			if ( i >= 0 ) pending.splice ( i, 1 );

			i = loading.indexOf ( loadObject.id );
			if ( i >= 0 ) loading.splice ( i, 1 );

			i = done.indexOf ( loadObject.id );
			if ( i >= 0 ) done.splice ( i, 1 );
		}
		
		/*
			Function: pause
				
				While loading is paused, any currently loading object will continue to
				load but after it is done, no objects will be pulled from the pending
		*/
		/**            
		 * @private
		 */
		internal function set pause ( value : Boolean ) : void
		{
			_pause = value;
		}
		
		/**            
		 * @private
		 */
		internal function get pause ( ) : Boolean
		{
			return _pause;
		}
		
		/*
			Function: checkLoadStatus
				
				This function is run every frame while we have assets pending or
				loading. Here we move assets from the pending state into the
				loading state as our limits allow us.
		*/
		/**            
		 * @private
		 */
		internal function checkLoadStatus ( ) : void
		{
			if ( pause ) return;
			
			// Deal with our demanded queue.
			//
			
			processDemandObjects ( );
			
			var objectIndex : uint = 0;
	
			if ( loading.length < getObjectOption ( "assetMgrLoadConnections" ) ) loadPendingObject ( getObjectOption ( "assetMgrLoadConnections" ) - loading.length );
	
			while ( objectIndex < loading.length )
			{
				var loadObject : Asset = all [ loading [ objectIndex ] ];
	
				if ( loadObject.state == Asset.LOADED || loadObject.state == Asset.FAILED )
				{
					loading.splice ( objectIndex, 1 );
					
					if ( loadObject.state == Asset.FAILED )
					{
						if ( loadObject.failures < getObjectOption ( "assetMgrMaxFailuresPerAsset" ) )
						{
							// Move this object to the end of the pending queue.
							// Make sure our load task is started.
							//
							
							pending.unshift ( loadObject.id );
							enableStatusTask ( true );
						}
						else
						{
							// Move this object to the end of the done queue.
							//
							
							Debugger.logMessage ( Debugger.INFO, "AssetManager", "gave up trying to load " + loadObject.url.url );
							
							done.unshift ( loadObject.id );
							
							loadObject.dispatchEvent ( new FrontalEvent ( FrontalEvent.FAILED ) );
						}
					}
					else
					{
						bytesLoaded += loadObject.bytesLoaded;
						loadingSecs += loadObject.loadTime / 1000;
			
						done.push ( loadObject.id );
	
						loadObject.dispatchEvent ( new FrontalEvent ( FrontalEvent.LOADED ) );
						
						if ( bytesLoaded > getObjectOption ( "assetMgrLoadLimitInBytes" ) )
						{
							// Stop the task if we've hit our limit. It will start
							// again when the next object is demanded.
							//
							
							enableStatusTask ( false );
						}
					}
				}
				
				objectIndex++;
			}
			
			if ( pending.length <= 0 && loading.length <= 0 ) enableStatusTask ( false );
		}
	}
}

